package com.inspectortime.calendar;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import com.inspectortime.domain.Inspection;
import com.inspectortime.domain.Inspector;
import com.inspectortime.domain.ScheduleRule;
import com.inspectortime.domain.type.InspectionStatus;
import com.inspectortime.domain.type.Time;
import com.inspectortime.service.ZipCodeSearchService;

/**
 * An in-memory representation of a single Inspector's calendar of available
 * timeslots for a given sequence of days
 * 
 * @author mKvalsvik
 * 
 */
public class InspectorCalendar {

	private List<Day> dayList;
	private Inspector inspector;
	private Date from;
	private Date to;

	/**
	 * Generate a list of Days populated with timeslots based on the Inspector's
	 * schedule rules
	 * 
	 * Note: Days in the past will not contain timeslots
	 * 
	 * @param from
	 *            the day in which to begin the calendar
	 * @param to
	 *            the day in which to end the calendar
	 * @param if true, will mark as unavailable any timeslots with date/time
	 *        that match an already scheduled appointment
	 * 
	 * @return list of days populated by the inspector's schedule rules
	 */
	public InspectorCalendar(Date fromDate, Date toDate, Inspector inspector) {
		this.inspector = inspector;
		this.from = fromDate;
		this.to = toDate;
		this.reset();
	}
	
	/**
	 * Resets the inspector calendar state completely from the schedule rules
	 */
	public void reset() {
		dayList = new ArrayList<Day>();
		initializeDayList();
		this.populateDayListFromInspector();
	}

	/**
	 * Generate a list of Days empty of timeslots or scheduleRules
	 */
	private void initializeDayList() {
		if (from.after(to)) {
			throw new IllegalArgumentException(
					"fromTime does not come before toTime");
		}

		// clone from cal so we can modify it without affecting the passed in
		// value
		Calendar fromCal = Calendar.getInstance();
		fromCal.setTime(from);

		while (!fromCal.getTime().after(to)) {

			Day day = new Day(fromCal.get(Calendar.MONTH), fromCal
					.get(Calendar.DATE), fromCal.get(Calendar.YEAR), fromCal
					.get(Calendar.DAY_OF_WEEK));
			dayList.add(day);
			fromCal.add(Calendar.DATE, 1);
		}
	}

	/**
	 * Populate a pre-existing list of days with timeslots based on the
	 * Inspector's schedule rules
	 * 
	 * @param days
	 */
	private void populateDayListFromInspector() {

		Calendar yesterdayCal = new GregorianCalendar();
		yesterdayCal.add(Calendar.DATE, -1);

		// just to be safe, order the schedule rules by priority
		Collections.sort(inspector.getScheduleRules());

		for (Day day : dayList) {

			// Only populate days that don't occur in the past
			if (day.isAfter(yesterdayCal)) {
				for (ScheduleRule rule : inspector.getScheduleRules()) {
					if (day.canPopulateFrom(rule)) {
						day.populateFrom(rule);
					}
				}

				if (day.hasTimeslot()) {
					Collections.sort(day.getTimeslots()); // sort the timeslots
					// in ascending
					// order
				}
			}
		}
	}

	public String toString() {
		StringBuilder sb = new StringBuilder();
		SimpleDateFormat sdf = new SimpleDateFormat("MMM-dd");
		for (Day day : dayList) {
			sb.append("" + sdf.format(day.toDate()) + ": ");
			List<Timeslot> ts = day.getTimeslots();
			boolean hasTimeslots = false;
			for (Timeslot t : ts) {
				sb.append(t.toString());
				sb.append(", ");
				hasTimeslots = true;
			}			
			// remove last comma
			if (hasTimeslots) {
				sb.deleteCharAt(sb.length()-1);
				sb.deleteCharAt(sb.length()-1);
				sb.append(" ");
			}
		}
		return sb.toString();
	}
	
	public Timeslot retrieveExactTimeslot(Date date, Time start, Time end) {
		List<Day> days = this.getDayList();
		for (Day day : days) {
			List<Timeslot> ts = day.getTimeslots();
			if (day.equalsDate(date)) {
				for (Timeslot t : ts) {
					if (t.getStartTime().equals(start) && t.getEndTime().equals(end)) {
						return t;
					}
				}
			}
		}
		return null;
	}	

	public void disableFarAwayTimeslots(
			ZipCodeSearchService zipCodeSearchService, String newInspectionZip) {

		// Option not enabled?
		if (!inspector.isLimitDrivingPerDay()) {
			return;
		}

		// Existing inspections
		List<Inspection> inspections = inspector.findInspectionsBetween(from,
				to);
		if (inspections == null || inspections.isEmpty()) {

			// No inspections booked, thus no need to disable any timeslots
			return;
		}

		// Loop over each day
		for (Day day : dayList) {
			for (Inspection inspection : inspections) {

				// only need to check scheduled inspections... Cancelled
				// inspections
				// should be considered "open" timeslots
				if (inspection.getInspectionStatus() == InspectionStatus.QUICK_BOOK
						|| inspection.getInspectionStatus() == InspectionStatus.SCHEDULED) {

					// if day is the same as the scheduled inspection...
					if (day.equalsDate(inspection.getInspectionTime())) {

						// Find the distance..
						double distance = zipCodeSearchService
								.findDistanceInMiles(newInspectionZip,
										inspection.getProperty().getLocation()
												.getZipCode());

						// If too far then mark all timeslots for that day as
						// unavailable
						if (distance > inspector
								.getLimitDrivingPerDayDistance()) {
							day.disableAllTimeslots();
						}
					}
				}
			}
		}
	}

	/**
	 * Remove timeslots that are too far away from existing inspections on a
	 * given day
	 */
	public void disableFarAwayTimeslotsForDay(String newInspectionZip, Day day,
			List<Inspection> inspections, Inspector inspector) {

	}

	public void markScheduledAsUnavailable() {

		// this is gross, but we have to bump the "to" date up one day,
		// so that the entire intended day is included in the query
		// (ie orginally the query was looking for all schedules up till
		// xx/xx/xx xx:xx,
		// so any schedules after xx:xx weren't included)
		Calendar cal = Calendar.getInstance();
		cal.setTime(to);
		cal.add(Calendar.DATE, +1);
		Date modifiedTo = cal.getTime();

		// Also modify the "from" time to ensure that very recent inspections
		// (i.e. today) are included and marked as unavailable
		cal.setTime(from);
		cal.add(Calendar.DATE, -1);
		Date modifiedFrom = cal.getTime();

		List<Inspection> inspections = inspector.findInspectionsBetween(
				modifiedFrom, modifiedTo);
		if (inspections != null) {
			boolean timeslotFound = false;
			for (Inspection inspection : inspections) {

				// only need to check scheduled inspections... Cancelled
				// inspections
				// should be considered "open" timeslots
				if (inspection.getInspectionStatus() == InspectionStatus.QUICK_BOOK
						|| inspection.getInspectionStatus() == InspectionStatus.SCHEDULED) {

					timeslotFound = false;

					for (Day day : dayList) {

						// if day is the same as the scheduled inspection...
						if (day.equalsDate(inspection.getInspectionTime())) {

							// ...loop thru timeslots and see if the scheduled
							// inspection
							// conflicts with the available timeslot
							if (day.hasTimeslot()) {

								for (Timeslot timeslot : day.getTimeslots()) {
									if (timeslot.containsTime(inspection
											.getInspectionTime())) {
										timeslot.setAvailable(false);
										timeslotFound = true;
										break;
									}
								} // for timeslot

							}

						}
						if (timeslotFound) {
							break;
						}
					} // for day

				} // status check
			} // for inspections
		}
	}

	public List<Day> getDayList() {
		return dayList;
	}

}